﻿using System;
using System.Windows;
using Dw = System.Drawing;
using Agora.Rtc;
using SharpDX;
using SharpDX.Direct3D9;
using System.Windows.Interop;
using System.Windows.Forms;
using System.Windows.Media.Media3D;
using System.Runtime.InteropServices;
using MyMeeting.CustomRender;

namespace MyMeeting.src.Advanced.CustomRender
{

namespace C_Sharp_API_Example.src.Advanced.CustomRender
    {
        public class SharpDxImgRender :BaseRender
        {
            // render
            private bool disposing_ = false;
            private IntPtr handle_ = IntPtr.Zero;
            private Dw.Size size_ = new Dw.Size();
            private DataType data_type_ = DataType.kYUV420;

            // d3d
            private SharpDX.Direct3D9.Direct3D d3d_ = new SharpDX.Direct3D9.Direct3D();
            private SharpDX.Direct3D9.Device device = null;
            private SharpDX.Direct3D9.Surface _offScreenSurface = null;
            private Texture texture;
            private SharpDX.Direct3D9.Surface d3d_backend_surface_ = null;
            private SharpDX.Direct3D9.Font d3d_fps_font_ = null;
            private Size offScrrenBufferSize = new Size();
            private Size backSurfaceBufferSize = new Size();

            // WPF D3DImage
            private D3DImage _d3dImage;
            private Form dummyWindow;
            private int imgW = 0, imgH = 0;

            public SharpDxImgRender()
            {
                this.dummyWindow = new Form();
                this.handle_ = this.dummyWindow.Handle;
            }
            public override void SetD3dImag(D3DImage img)
            {
                this._d3dImage = img;
                _d3dImage.Dispatcher.Invoke(() =>
                {
                    _d3dImage.IsFrontBufferAvailableChanged += D3DImage_IsFrontBufferAvailableChanged;
                    imgW = (int)_d3dImage.Width;
                    imgH = (int)_d3dImage.Height;

                });
            }
            
            #region override BaseRender
            public override void Dispose()
            {
                if (disposing_ == true)
                    return;

                disposing_ = true;

                StopFpsTimer();

                if (d3d_fps_font_ != null)
                    d3d_fps_font_.Dispose();

                if (_offScreenSurface != null)
                    _offScreenSurface.Dispose();

                if (d3d_backend_surface_ != null)
                    d3d_backend_surface_.Dispose();

                if (device != null)
                    device.Dispose();

                d3d_.Dispose();
                this.SafeRelease(this.dummyWindow);
                this.SafeRelease(this.texture);
            }
            private void SafeRelease(IDisposable item)
            {
                try
                {
                    if (item != null)
                    {
                        item.Dispose();
                    }
                }
                catch
                {
                }
            }
            public override CustomVideoBoxRenderType GetRenderType()
            {
                return data_type_ == DataType.kBGRA ? CustomVideoBoxRenderType.kSharpDX_BGRA : CustomVideoBoxRenderType.kSharpDX_YUV420;
            }

            public override bool Initialize(IntPtr handle, Dw.Size size, DataType dataType)
            {
                //handle_ = handle;
                size_ = size;
                data_type_ = dataType;

                if (!AutoCreateD3DDevice(1920, 1080, size.Width, size.Height))
                {
                    return false;
                }

                StartFpsTimer();

                return true;
            }
            private PresentParameters GetPresentParameters(int width, int height,Format format)
            {
                PresentParameters presentParams = new PresentParameters();
                presentParams.PresentFlags = PresentFlags.Video;
                presentParams.Windowed = true;
                presentParams.DeviceWindowHandle = this.handle_;
                presentParams.BackBufferWidth = width == 0 ? 1 : width;
                presentParams.BackBufferHeight = height == 0 ? 1 : height;
                presentParams.SwapEffect = SwapEffect.Discard;
                //presentParams.Multisample = MultisampleType.NonMaskable;
                presentParams.PresentationInterval = PresentInterval.Immediate;
                presentParams.BackBufferFormat = format;
                presentParams.BackBufferCount = 1;
                presentParams.EnableAutoDepthStencil = false;

                return presentParams;
            }
            public override void UpdateSize(Dw.Size size)
            {
                size_ = size;
            }

            public override void DeliverFrame(VideoFrame videoFrame)
            {
                if (disposing_) return;

                if (data_type_ == DataType.kBGRA && videoFrame.type != VIDEO_PIXEL_FORMAT.VIDEO_PIXEL_BGRA && videoFrame.type != VIDEO_PIXEL_FORMAT.VIDEO_PIXEL_RGBA)
                    return;

                if (data_type_ == DataType.kYUV420 && videoFrame.type != VIDEO_PIXEL_FORMAT.VIDEO_PIXEL_I420)
                    return;

                if (AutoCreateD3DDevice(videoFrame.width, videoFrame.height, size_.Width, size_.Height) != true)
                {
                    Console.WriteLine("d3d failed to create d3d device when deliver frame");
                    return;
                }

                #region lock offscreen surface
                SharpDX.DataRectangle locked_data = new DataRectangle();

                const int MAX_TRY = 3;
                int times = 0;
                do
                {
                    bool need_retry = false;
                    try
                    {
                        locked_data = _offScreenSurface.LockRectangle(SharpDX.Direct3D9.LockFlags.DoNotWait);
                    }
                    catch (SharpDX.SharpDXException e)
                    {
                        // D3DERR_WASSTILLDRAWING
                        if (e.Message.Contains("WASSTILLDRAWING"))
                            need_retry = true;
                        else
                        {
                            Console.WriteLine("d3d lock surface failed {0}", e.Message);
                            return;
                        }
                    }

                    if (need_retry != true)
                        break;

                    times++;

                    // should sleep in microseconds

                } while (times < MAX_TRY);

                if (locked_data.DataPointer == IntPtr.Zero)
                {
                    Console.WriteLine("d3d failed to lock offscreen surface");
                    return;
                }

                if (data_type_ == DataType.kBGRA)
                {
                    for (int i = 0; i < videoFrame.height; i++)
                    {
                        CopyMemory(locked_data.DataPointer + locked_data.Pitch * i, videoFrame.yBufferPtr + i * videoFrame.yStride, Math.Min(videoFrame.yStride, locked_data.Pitch));
                    }
                }
                else if (data_type_ == DataType.kYUV420)
                {
                    // copy y data with stride
                    IntPtr dst_y_ptr = locked_data.DataPointer;
                    for (int i = 0; i < videoFrame.height; i++)
                    {
                        CopyMemory(dst_y_ptr + i * locked_data.Pitch, videoFrame.yBufferPtr + i * videoFrame.yStride, Math.Min(videoFrame.yStride, locked_data.Pitch));
                    }

                    // coz dst data is yv12
                    // copy v data with stride
                    IntPtr dst_u_ptr = dst_y_ptr + locked_data.Pitch * videoFrame.height;
                    for (int i = 0; i < videoFrame.height / 2; i++)
                    {
                        CopyMemory(dst_u_ptr + i * locked_data.Pitch / 2, videoFrame.vBufferPtr + i * videoFrame.vStride, Math.Min(videoFrame.vStride, locked_data.Pitch / 2));
                    }

                    // coz dst data is yv12
                    // copy u data with stride
                    IntPtr dst_v_ptr = dst_u_ptr + locked_data.Pitch / 2 * videoFrame.height / 2;
                    for (int i = 0; i < videoFrame.height / 2; i++)
                    {
                        CopyMemory(dst_v_ptr + i * locked_data.Pitch / 2, videoFrame.uBufferPtr + i * videoFrame.uStride, Math.Min(videoFrame.uStride, locked_data.Pitch / 2));
                    }
                }

                _offScreenSurface.UnlockRectangle();
                #endregion // lock offscreen surface

                #region present
                // clear with black
                device.Clear(SharpDX.Direct3D9.ClearFlags.Target, new SharpDX.Mathematics.Interop.RawColorBGRA(0, 0, 0, 255), 1.0f, 0);

                // begin scene
                device.BeginScene();
               
                // stretch copy
                System.Drawing.Rectangle dest_rect = CalcDestRectangle(new Size(videoFrame.width, videoFrame.height), backSurfaceBufferSize);
                SharpDX.Mathematics.Interop.RawRectangle src_native_rect = new SharpDX.Mathematics.Interop.RawRectangle(0, 0, videoFrame.width, videoFrame.height);
                SharpDX.Mathematics.Interop.RawRectangle dst_native_rect = new SharpDX.Mathematics.Interop.RawRectangle(OddRectangleValue(dest_rect.X), OddRectangleValue(dest_rect.Y), OddRectangleValue(dest_rect.Right), OddRectangleValue(dest_rect.Bottom));
                //device.StretchRectangle(_offScreenSurface, src_native_rect, d3d_backend_surface_, dst_native_rect, SharpDX.Direct3D9.TextureFilter.Linear);
                this.device.StretchRectangle(this._offScreenSurface, this.d3d_backend_surface_, TextureFilter.Linear);

                if (d3d_fps_font_ != null)
                {
                    string tips_fps = "FPS:" + GetCurrentFps().ToString();
                    SharpDX.Mathematics.Interop.RawRectangle font_rect = d3d_fps_font_.MeasureText(null,
                        tips_fps,
                        new SharpDX.Mathematics.Interop.RawRectangle(0, 0, (int)backSurfaceBufferSize.Width, (int)backSurfaceBufferSize.Height),
                        SharpDX.Direct3D9.FontDrawFlags.Center | SharpDX.Direct3D9.FontDrawFlags.VerticalCenter);
                    d3d_fps_font_.DrawText(null,
                        tips_fps,
                        (int)(backSurfaceBufferSize.Width - (font_rect.Right - font_rect.Left) - 10),
                        10,
                        new SharpDX.Mathematics.Interop.RawColorBGRA(0, 0, 255, 255));
                }
                this.device.SetTexture(0, this.texture);
               // this.device.DrawPrimitives(PrimitiveType., 0, 2);
                // end scene
                device.EndScene();

                
                // Update D3DImage
                if (_d3dImage != null)
                {
                    _d3dImage.Dispatcher.Invoke(() =>
                    {
                        _d3dImage.Lock();
                        _d3dImage.SetBackBuffer(D3DResourceType.IDirect3DSurface9, d3d_backend_surface_.NativePointer);
                        _d3dImage.AddDirtyRect(new Int32Rect(0, 0, _d3dImage.PixelWidth, _d3dImage.PixelHeight));
                        _d3dImage.Unlock();
                    });
                }

                // present
                device.Present();
                #endregion // present

                FeedOneFrame();
            }

            public override bool NeedInvoke()
            {
                return false;
            }

            public override void Clear()
            {
                device.Clear(SharpDX.Direct3D9.ClearFlags.Target, new SharpDX.Mathematics.Interop.RawColorBGRA(0, 0, 0, 255), 1.0f, 0);
            }


            private void D3DImage_IsFrontBufferAvailableChanged(object sender, DependencyPropertyChangedEventArgs e)
            {
                if (_d3dImage.IsFrontBufferAvailable)
                {
                    // Re-set the back buffer when the front buffer becomes available
                    _d3dImage.Lock();
                    _d3dImage.SetBackBuffer(D3DResourceType.IDirect3DSurface9, d3d_backend_surface_.NativePointer);
                    _d3dImage.Unlock();
                }
            }
            #endregion // override BaseRender

            #region base d3d functions
            private bool EnumValidAdapter(ref int adapter_index, ref SharpDX.Direct3D9.Format adapter_format, ref SharpDX.Direct3D9.DeviceType device_type)
            {
                const int D3DADAPTER_DEFAULT = 0;

                SharpDX.Direct3D9.DisplayMode mode = d3d_.GetAdapterDisplayMode(D3DADAPTER_DEFAULT);
                if (mode.Format == SharpDX.Direct3D9.Format.X8R8G8B8 &&
                    !d3d_.CheckDeviceType(D3DADAPTER_DEFAULT, SharpDX.Direct3D9.DeviceType.Hardware, mode.Format, mode.Format, true))
                {
                    Console.WriteLine("d3d check device type failed");
                }

                adapter_format = mode.Format;

                // set default preferences
                adapter_index = D3DADAPTER_DEFAULT;
                device_type = SharpDX.Direct3D9.DeviceType.Hardware;

                // we prefer to use NVIDIA
                foreach (var adapter in d3d_.Adapters)
                {
                    var identifier = d3d_.GetAdapterIdentifier(adapter.Adapter);
                    if (identifier.Description.Contains("PerfHUD"))
                    {
                        adapter_index = adapter.Adapter;
                        device_type = SharpDX.Direct3D9.DeviceType.Reference;
                        break;
                    }
                }

                var adapter_identifier = d3d_.GetAdapterIdentifier(adapter_index);
                Console.WriteLine("d3d decide to use adapter: {0} \r\n" +
                    " Description: {1}\r\n" +
                    " Driver: {2} \r\n" +
                    " VendorId: {3}\r\n" +
                    " DeviceId: {4}\r\n",
                    adapter_index,
                    adapter_identifier.Description,
                    adapter_identifier.Driver,
                    adapter_identifier.VendorId,
                    adapter_identifier.DeviceId);

                return true;
            }

            private bool AutoCreateD3DDevice(int frame_width, int frame_height, int wnd_width, int wnd_height)
            {
                if (backSurfaceBufferSize.Width == wnd_width && backSurfaceBufferSize.Height == wnd_height &&
                    offScrrenBufferSize.Width == frame_width && offScrrenBufferSize.Height == frame_height)
                    return true;

                if (d3d_fps_font_ != null)
                    d3d_fps_font_.Dispose();

                if (_offScreenSurface != null)
                    _offScreenSurface.Dispose();

                if (d3d_backend_surface_ != null)
                    d3d_backend_surface_.Dispose();

                if (device != null)
                    device.Dispose();

                int adapter_index = -1;
                SharpDX.Direct3D9.Format adapter_format = SharpDX.Direct3D9.Format.X8R8G8B8;
                SharpDX.Direct3D9.DeviceType device_type = SharpDX.Direct3D9.DeviceType.Hardware;

                // enum a valid adapter to use
                if (!EnumValidAdapter(ref adapter_index, ref adapter_format, ref device_type) ||
                    adapter_index == -1)
                {
                    Console.WriteLine("d3d enum valid adapter failed with index: {0}", adapter_index);
                    return false;
                }
                SharpDX.Direct3D9.PresentParameters present_params = GetPresentParameters(wnd_width, wnd_height, adapter_format);
                // create d3d device, should recreate device when width or height of frame changed
                //SharpDX.Direct3D9.PresentParameters present_params = new SharpDX.Direct3D9.PresentParameters(
                //    wnd_width /* backBufferWidth */,
                //    wnd_height /* backBufferHeight */,
                //    adapter_format /* backBufferFormat */,
                //    1 /* backBufferCount */,
                //    SharpDX.Direct3D9.MultisampleType.None /* multiSampleType */,
                //    0 /* multiSampleQuality */,
                //    SharpDX.Direct3D9.SwapEffect.Copy /* swapEffect */,
                //    handle_ /* deviceWindowHandle */,
                //    true /* windowed */,
                //    false /* enableAutoDepthStencil */,
                //    SharpDX.Direct3D9.Format.Unknown /* autoDepthStencilFormat */,
                //    SharpDX.Direct3D9.PresentFlags.Video /* presentFlags */,
                //    0 /* fullScreenRefreshRateInHz */,
                //    SharpDX.Direct3D9.PresentInterval.Default /* presentationInterval */);

                device = new SharpDX.Direct3D9.Device(d3d_, adapter_index, device_type, IntPtr.Zero,
                    SharpDX.Direct3D9.CreateFlags.Multithreaded | SharpDX.Direct3D9.CreateFlags.SoftwareVertexProcessing,
                    present_params);

                SharpDX.Direct3D9.FontDescription font_description = new SharpDX.Direct3D9.FontDescription()
                {
                    Height = 24,
                    Italic = false,
                    CharacterSet = SharpDX.Direct3D9.FontCharacterSet.Ansi,
                    FaceName = "Arial",
                    MipLevels = 0,
                    OutputPrecision = SharpDX.Direct3D9.FontPrecision.TrueType,
                    PitchAndFamily = SharpDX.Direct3D9.FontPitchAndFamily.Default,
                    Quality = SharpDX.Direct3D9.FontQuality.ClearType,
                    Weight = SharpDX.Direct3D9.FontWeight.Thin
                };

                d3d_fps_font_ = new SharpDX.Direct3D9.Font(device, font_description);

                _offScreenSurface = SharpDX.Direct3D9.Surface.CreateOffscreenPlain(device,
                    frame_width,
                    frame_height,
                    data_type_ == DataType.kBGRA ? SharpDX.Direct3D9.Format.X8R8G8B8 : (SharpDX.Direct3D9.Format)842094169 /* MAKEFOURCC('Y', 'V', '1', '2') */,
                    SharpDX.Direct3D9.Pool.Default);
                this.texture = new Texture(this.device, wnd_width, wnd_height, 1, Usage.RenderTarget, adapter_format, Pool.Default);
                d3d_backend_surface_ = this.texture.GetSurfaceLevel(0);

                device.SetRenderState(SharpDX.Direct3D9.RenderState.CullMode, 1 /* D3DCULL_NONE = 1 */);
                device.SetRenderState(SharpDX.Direct3D9.RenderState.Lighting, false);

                offScrrenBufferSize.Width = frame_width;
                offScrrenBufferSize.Height = frame_height;

                backSurfaceBufferSize.Width = wnd_width;
                backSurfaceBufferSize.Height = wnd_height;

                return true;
            }
            #endregion // base d3d functions
           
        }
    }
}
